/**
 * Copyright (c) 2016-2023, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jpress.pro.filesync;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.jfinal.kit.HashKit;
import com.jfinal.kit.PathKit;
import io.jboot.Jboot;
import io.jboot.utils.FileUtil;
import io.jboot.utils.HttpUtil;
import io.jboot.utils.NamedThreadPools;
import io.jboot.utils.StrUtil;
import io.jpress.JPressConfig;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;

import java.io.File;
import java.io.FileFilter;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author michael yang (fuhai999@gmail.com)
 * @Date: 2020/3/17
 */
public class SyncFilesWatcher extends Thread {

    private static final SyncFilesWatcher me = new SyncFilesWatcher();

    public static SyncFilesWatcher me() {
        return me;
    }

    private int watchingInterval = 100;
    private ExecutorService threadPool = NamedThreadPools.newFixedThreadPool(3, "jpress-file-sync");

    private Set<String> hosts = StrUtil.splitToSet(FileSyncConfig.getInstance().getRemoteHosts(), ";");
    private int webRootIndexOf = PathKit.getWebRootPath().length();
    private String httpScheme = FileSyncConfig.getInstance().getHttpScheme();
    private String httpActionPath = FileSyncConfig.getInstance().getHttpActionPath();

    private String secret = FileSyncConfig.getInstance().getSecret();
    private boolean isSyncAttachmentDir = FileSyncConfig.getInstance().isSyncAttachmentDirEnable();


    protected Cache<String, ReentrantLock> ignoreFileLocks = Caffeine.newBuilder()
            .expireAfterWrite(60, TimeUnit.SECONDS)
            .build();


    private SyncFilesWatcher() {
        setName("SyncFilesWatcher");
        setDaemon(false);
        setPriority(Thread.MAX_PRIORITY);
    }


    @Override
    public void run() {
        try {
            doRun();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }


    protected void doRun() throws Exception {

        FileAlterationObserver webRootObserver = createWebRootObserver();
        FileAlterationObserver addonsObserver = createAddonsObserver();
        FileAlterationObserver templatesObserver = createTemplatesObserver();

        Set<FileAlterationObserver> observers = new HashSet<>();
        observers.add(webRootObserver);
        observers.add(addonsObserver);
        observers.add(templatesObserver);


        if (isSyncAttachmentDir) {
            observers.add(createAttachmentsObserver());
        }

        FileAlterationMonitor fileMonitor = new FileAlterationMonitor(watchingInterval
                , observers.toArray(new FileAlterationObserver[observers.size()]));
        addShutdownHook(fileMonitor);

        fileMonitor.start();
    }

    private FileAlterationObserver createWebRootObserver() {
        //只扫描根目录
        FileFilter webRootFilter = new FileFilter() {
            private File webRootFile = new File(PathKit.getWebRootPath());

            @Override
            public boolean accept(File file) {
                return file.getParentFile().equals(webRootFile);
            }
        };

        FileAlterationObserver webRootObserver = new FileAlterationObserver(PathKit.getWebRootPath(), webRootFilter);
        webRootObserver.addListener(new FileListener() {
            @Override
            public void onFileCreate(File file) {
                doFileCreate(file, webRootIndexOf, FileTypes.WEBROOT);
            }

            @Override
            public void onFileChange(File file) {
                doFileChange(file, webRootIndexOf, FileTypes.WEBROOT);
            }

            @Override
            public void onFileDelete(File file) {
                doFileDelete(file, webRootIndexOf, FileTypes.WEBROOT);
            }
        });
        return webRootObserver;
    }


    private FileAlterationObserver createAddonsObserver() {
        //只扫描根目录
        FileFilter addonRootFilter = new FileFilter() {
            private String addonRootPath = FileUtil.getCanonicalPath(new File(JPressConfig.me.getAddonRoot(), "addons"));
            private String addonUnzipRootPath = FileUtil.getCanonicalPath(new File(JPressConfig.me.getAddonRoot(), "WEB-INF/addons"));

            @Override
            public boolean accept(File file) {
                String absolutePath = file.getAbsolutePath();
                return absolutePath.startsWith(addonRootPath) || absolutePath.startsWith(addonUnzipRootPath);
            }
        };

        FileAlterationObserver addonObserver = new FileAlterationObserver(
                FileUtil.getCanonicalPath(new File(JPressConfig.me.getAddonRoot())), addonRootFilter);

        final int addonIndexOf = FileUtil.getCanonicalPath(new File(JPressConfig.me.getAddonRoot())).length();
        addonObserver.addListener(new FileListener() {
            @Override
            public void onFileCreate(File file) {
                doFileCreate(file, addonIndexOf, FileTypes.ADDON);
            }

            @Override
            public void onFileChange(File file) {
                doFileChange(file, addonIndexOf, FileTypes.ADDON);
            }

            @Override
            public void onFileDelete(File file) {
                doFileDelete(file, addonIndexOf, FileTypes.ADDON);
            }
        });
        return addonObserver;
    }


    private FileAlterationObserver createTemplatesObserver() {
        FileFilter fileFilter = new FileFilter() {
            private String rootPath = FileUtil.getCanonicalPath(new File(JPressConfig.me.getTemplateRoot(), "templates"));

            @Override
            public boolean accept(File file) {
                return file.getAbsolutePath().startsWith(rootPath);
            }
        };

        FileAlterationObserver observer = new FileAlterationObserver(
                FileUtil.getCanonicalPath(new File(JPressConfig.me.getTemplateRoot())), fileFilter);

        final int templateIndexOf = FileUtil.getCanonicalPath(new File(JPressConfig.me.getTemplateRoot())).length();
        observer.addListener(new FileListener() {
            @Override
            public void onFileCreate(File file) {
                doFileCreate(file, templateIndexOf, FileTypes.TEMPLATE);
            }

            @Override
            public void onFileChange(File file) {
                doFileChange(file, templateIndexOf, FileTypes.TEMPLATE);
            }

            @Override
            public void onFileDelete(File file) {
                doFileDelete(file, templateIndexOf, FileTypes.TEMPLATE);
            }
        });
        return observer;
    }


    private FileAlterationObserver createAttachmentsObserver() {
        FileFilter fileFilter = new FileFilter() {
            private String rootPath = FileUtil.getCanonicalPath(new File(JPressConfig.me.getAttachmentRoot(), "attachment"));

            @Override
            public boolean accept(File file) {
                return file.getAbsolutePath().startsWith(rootPath);
            }
        };

        FileAlterationObserver observer = new FileAlterationObserver(
                FileUtil.getCanonicalPath(new File(JPressConfig.me.getAttachmentRoot())), fileFilter);

        final int subIndexOf = FileUtil.getCanonicalPath(new File(JPressConfig.me.getAttachmentRoot())).length();
        observer.addListener(new FileListener() {
            @Override
            public void onFileCreate(File file) {
                doFileCreate(file, subIndexOf, FileTypes.ATTACHMENT);
            }

            @Override
            public void onFileChange(File file) {
                doFileChange(file, subIndexOf, FileTypes.ATTACHMENT);
            }

            @Override
            public void onFileDelete(File file) {
                doFileDelete(file, subIndexOf, FileTypes.ATTACHMENT);
            }
        });
        return observer;
    }


    private void doFileCreate(File file, int subIndex, int fileType) {
        ReentrantLock lock = ignoreFileLocks.getIfPresent(file.getAbsolutePath());
        if (lock != null && lock.isLocked()) {
            //如果解锁了，移除
            return;
        } else {
            ignoreFileLocks.invalidate(file.getAbsolutePath());
        }

        //这个创建的文件来源于其他 server 同步的
        if (RemoteFilesRecord.isCreatedRecord(file)){
            return;
        }

        if (hosts != null && hosts.size() > 0) {
            if (Jboot.isDevMode()) {
                System.err.println("fileCreate: " + file.getAbsolutePath().substring(subIndex));
            }
            for (String host : hosts) {
                String fileName = file.getAbsolutePath().substring(subIndex);
                String url = createUrl(host, "create", fileName, fileType, file.lastModified());
                threadPool.submit(() -> {
                    if (file.exists()) {
                        HttpUtil.upload(url, file);
                    }
                });
            }
        }
    }


    public void doFileChange(File file, int subIndex, int fileType) {
        ReentrantLock lock = ignoreFileLocks.getIfPresent(file.getAbsolutePath());
        if (lock != null && lock.isLocked()) {
            //如果解锁了，移除
            return;
        } else {
            ignoreFileLocks.invalidate(file.getAbsolutePath());
        }

        //这个创建的文件来源于其他 server 同步的
        if (RemoteFilesRecord.isChangedRecord(file)){
            return;
        }

        if (hosts != null && hosts.size() > 0) {
            if (Jboot.isDevMode()) {
                System.err.println("fileChange: " + file.getAbsolutePath().substring(subIndex));
            }
            for (String host : hosts) {
                String fileName = file.getAbsolutePath().substring(subIndex);
                String url = createUrl(host, "change", fileName, fileType, file.lastModified());
                threadPool.submit(() -> {
                    if (file.exists()) {
                        HttpUtil.upload(url, file);
                    }
                });
            }
        }
    }


    public void doFileDelete(File file, int subIndex, int fileType) {
        ReentrantLock lock = ignoreFileLocks.getIfPresent(file.getAbsolutePath());
        if (lock != null && lock.isLocked()) {
            //如果解锁了，移除
            return;
        } else {
            ignoreFileLocks.invalidate(file.getAbsolutePath());
        }

        if (RemoteFilesRecord.isDeleteRecord(file)){
            return;
        }

        if (hosts != null && hosts.size() > 0) {
            if (Jboot.isDevMode()) {
                System.err.println("fileDelete: " + file.getAbsolutePath().substring(subIndex));
            }
            for (String host : hosts) {
                String fileName = file.getAbsolutePath().substring(subIndex);
                String url = createUrl(host, "delete", fileName, fileType, 0);
                threadPool.submit(() -> {
                    HttpUtil.httpGet(url);
                });
            }
        }
    }


    private String createUrl(String host, String method, String fileName, int fileType, long lastModified) {
        StringBuilder url = new StringBuilder(httpScheme).append("://")
                .append(host)
                .append(httpActionPath)
                .append(httpActionPath.endsWith("/") ? "" : "/")
                .append(method)
                .append("?fileName=").append(StrUtil.urlEncode(fileName))
                .append("&type=").append(fileType)
                .append("&lastModified=").append(lastModified)
                .append("&sign=").append(HashKit.md5(fileName + fileType + lastModified + secret));

        return url.toString();
    }


    public void addIgnoreFile(File file, ReentrantLock lock) {
        ignoreFileLocks.put(file.getAbsolutePath(), lock);
    }


    protected void addShutdownHook(FileAlterationMonitor fileAlterationMonitor) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                fileAlterationMonitor.stop();
            } catch (Exception e) {
            }
        }));
    }


}
